package server;

import static server.RLM_Server.activeConnections;
import static server.RLM_Server.databaseModul;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.concurrent.Semaphore;

import connection_modul.DatabaseConnection;

import server_commands.Command;
import server_commands.LOGOUTCommand;
import utils.ConnectionBuffer;
import utils.Constants;
import utils.GenerateID;

/**
 * Klasa konekcije izmedju servera i klijenta. Svaka konekcija sadrzi referencu
 * na kreirani socket, IO stream-ove socket-a, jedinstveni ID konekcije,
 * username logovanog korisnika (ako je logovan) koji ce se koristiti u
 * slanju informacija sa servera ka klijentu, u slucaju da server sam
 * inicira slanje podataka, a ne kao odgovor na klijentom zahtev, i najzad,
 * dva thread-a, Reader i Writer, koji su odgovorni za pisanje i citanje
 * podataka u/iz konekijce.
 * 
 * @author Uros Krkic
 */
public class ServerSocketConnection implements Constants {

	/**
	 * Semafor koji obezbedjuje da getMessage() metod bude blokiran dok ne pristigne
	 * poruka za slanje, tj. dok se ne pozove setMessage() metod.
	 */
	private Semaphore waitForMessage = new Semaphore(1, true);
	
	
	/**
	 * Semafor obezbedjuje da ce se pri startovanju Reader i Writer thread-a, pre izvrsiti
	 * getMessage() metod, nego setMessage(), jer su pre kreiranja i startovanja thread-ova
	 * oba semafora zakljucana.
	 */
	private Semaphore messageAccepted = new Semaphore(1, true);
	
	
	/**
	 * Buffer za citanje i pisanje podataka u socket. Koristi se u setMessage() i getMessage()
	 * metodi, kao amortizer brzina poziva vise setMessage() metoda i brzine ispisa poruke
	 * u stream iz Writer thread-a kada getMessage() vrati poruku. 
	 */
	private ConnectionBuffer buffer = new ConnectionBuffer(1024);
	
	
	/**
	 * Socket dodeljen konekciji.
	 */
	private Socket socket;
	
	
	/**
	 * Stream od klijenta ka serveru.
	 */
	private BufferedReader inFromClient;
	
	
	/**
	 * Stream od servera ka klijentu.
	 */
	private PrintWriter outToClient;
	
	
	/**
	 * Jedinstevni ID konekcije.
	 */
	private long connectionID = 0;
	
	
	/**
	 * Username korisnika koji se logovao. Koristiti se pri slanju informacija sa
	 * servera ka klijentu, u slucaju da server sam inicira slanje podataka, a ne
	 * kao odgovor na klijentov zahtev. Po ovom username-u, pronalazi se konekcija
	 * iz vektora aktivnih konekcija, kojoj treba da se posalje podatak.
	 * 
	 * @author Uros Krkic
	 */
	private String username;
	
	
	/**
	 * Thread zaduzen za citanje podataka iz konekcije.
	 * @author Uros Krkic
	 */
	private ServerThreadReader serverThreadReader;
	
	
	/**
	 * Thread zaduzez za pisanje podataka u konekciju.
	 */
	private ServerThreadWriter serverThreadWriter;	

	/**
	 * Konstruktor.<br>
	 * Kreira instancu ServerSocketConnection klase i vrsi inicijalizacije atributa klase
	 * (objekta).<br>
	 * @param socket - socket koji je dodeljen konekciji
	 * @author Uros Krkic
	 */
	public ServerSocketConnection(Socket socket) {
		this.socket = socket;
		
		this.connectionID = GenerateID.generateID();
		
		initializeStreams();
		
		createReaderAndWriteThread();
	}
	
	
	/**
	 * Metod vraca jedinstveni ID konekcije.
	 * @return - ID konekcije
	 * @author Uros Krkic
	 */
	public long getConnectionID() {
		return this.connectionID;
	}
	
	
	/**
	 * Metod setuje atrubut username, na prosledjeni username logovanog korisnika.
	 * @param username - korisnicko ime logovanog korisnika
	 * @author Uros Krkic
	 */
	public void setUsername(String username) {
		this.username = username;
	}
	
	
	/**
	 * Metod vraca username logovanog korisnika koji je vlasnik konekcije.
	 * @return - username koji je vlasnik konekcije
	 * @author Uros Krkic
	 */
	public String getUsername() {
		return this.username;
	}
	
	
	
	/**
	 * Metod kreira Reader i Writer thread. Inicijalno se zakljucavaju oba semafora, kako bi
	 * se obezbedilo da ce se getMessage() metod izvrsiti pre nego sto neko pozove setMessage().
	 * @author Uros Krkic
	 */
	private void createReaderAndWriteThread() {
		try {
			waitForMessage.acquire();
			messageAccepted.acquire();
		}
		catch (InterruptedException ie) {
			ie.printStackTrace();
		}
		
		serverThreadReader = new ServerThreadReader(this);
		serverThreadWriter = new ServerThreadWriter(this);
	}
	

	
	/**
	 * Metoda koja pise podatke u konekciju (socket). Koristi se za slanje podataka
	 * od servera ka klijentu.
	 * @param data - string koji ce biti poslat klijentu
	 * @author Uros Krkic
	 */
	public void writeData(String data) {
		outToClient.println(data);
	}
	

	
	/**
	 * Metod cita podatke iz ulaznog stream-a sa klijenta.
	 * @return - procitani podaci sa servera
	 * @author Uros Krkic
	 */
	public String readData() {
		String response = null;
		
		try {
			response = this.inFromClient.readLine();
		}
		catch (IOException ioe) {
//			ioe.printStackTrace();

			/*
			 * Logout korisnika sa kim je konekcija prekinuta nasilno.
			 */
			Command logoutCommand = new LOGOUTCommand(username);
			DatabaseConnection dbConnection = new DatabaseConnection(databaseModul);
			if ( dbConnection.isClosed() )
				logoutCommand.executeCommand(this, dbConnection);
		}
		
		return response;
	}
	
	
	
	/**
	 * Metod vrsi inicijalizaciju stream-ova.
	 * @author Uros Krkic
	 */
	private void initializeStreams() {
		
		try {
			inFromClient = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			outToClient = new PrintWriter(socket.getOutputStream(), true);
		}
		catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}

	
	/**
	 * Vraca referencu na socket koji je pridruzen konekciji.
	 * @return - socket
	 */
	public Socket getSocket() {
		return socket;
	}
	
	
	/**
	 * Metod zatvara konekciju. Pod zatvaranjem konekcije podrazumeva se
	 * zatvaranje streamova, zatvaranje socket-a izmedju klijenta i
	 * servera, kao i stopiranje Reader i Writer thread-ova.
	 * Takodje, prekinuta konekcija se uklanja iz vektora aktivnih
	 * konekcija.
	 * @author Uros Krkic
	 */
	public void quitConnection() {
		try {
			this.socket.shutdownInput();
			this.socket.shutdownOutput();
			serverThreadWriter.interrupt();
			activeConnections.remove(this.getUsername());
			this.socket.close();
			this.inFromClient.close();
			this.outToClient.close();
			this.socket.close();
			serverThreadReader.interrupt();
		}
		catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}


	
	/**
	 * Metod koji inicira slanje poruke klijentu. Kada se pozove ovaj metod, poruka
	 * prosledjena kao argument upisuje se u buffer, i poziva se release() semafora, kako bi
	 * se obavestio uspavani Writer thread da je poruka spremna za slanja i nakon
	 * toga Writer thread vrsi slanje poruke, tj. pisanje poruke u stream.
	 * @param message - poruka koja se salje
	 * @param index - index buffer-a u koji ce Reader thread da upisuje podatke
	 * koje procita iz stream-a.
	 * @author Uros Krkic
	 */
	public synchronized void setMessage(String message) {

		buffer.write(message);
				
		waitForMessage.release();
		
		try {
			messageAccepted.acquire();
		}
		catch (InterruptedException ie) {
			ie.printStackTrace();
		}
	}


	/**
	 * Ovaj metod se uvek poziva na pocetku svake iteracije Writer threada. Pozivom
	 * ove metode, Writer thead se odmah stavlja u block stanje (semaforom), dok ne
	 * pristigne poruka koju treba poslati, a to se desava kada se pozove metod setMessage().
	 * release() iz metoda setMessage() uzrokuje da Writer thread nastavi izvrsavanje
	 * kroz ovaj metod, sto uzrokuje da ce metod vratiti poruku postavljenu setMessage()
	 * metodom. Nakon toga, Writer thread je dobio poruku, moze da je ispise u stream,
	 * nakon cega se proces ponavlja.
	 * Poruke se uzimaju iz buffer-a koji se puni setMessage() metodom. Na ovaj nacin
	 * je izvrsena amortizacija brzina u pozivu setMessage() metoda i ispisa poruka
	 * u stream.
	 * @return - poruka koja se ispisuje u stream
	 * @author Uros Krkic
	 */
	public String getMessage() {
		String tempMessage = "";
		try {
			waitForMessage.acquire();

			tempMessage = buffer.read();
			
			messageAccepted.release();
		}
		catch (InterruptedException ex) {
//			ex.printStackTrace();
			return null;
		}
		catch (Exception e) {
			e.printStackTrace();
		}
		
		return tempMessage;
	}
	
}
